﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AGB.Misc
{
    /// <summary>
    /// WeakHashtable
    /// </summary>
    /// <devdoc>
    /// This is a hashtable that stores object keys as weak references.
    /// It monitors memory usage and will periodically scavenge the
    /// hash table to clean out dead references.
    /// </devdoc>
    public sealed class WeakHashtable : Hashtable
    {
        private static readonly IEqualityComparer equalityComparer = new WeakKeyComparer();

        private long lastGlobalMem;
        private int lastHashCount;

        public WeakHashtable()
            : base(equalityComparer)
        {
        }

        /// <summary>
        /// Removes the element with the specified key from the <see cref="T:System.Collections.Hashtable"/>.
        /// </summary>
        /// <param name="key">The key of the element to remove.</param>
        /// <exception cref="T:System.ArgumentNullException">
        ///     <paramref name="key"/> is null.
        /// </exception>
        /// <exception cref="T:System.NotSupportedException">
        /// The <see cref="T:System.Collections.Hashtable"/> is read-only.
        /// -or-
        /// The <see cref="T:System.Collections.Hashtable"/> has a fixed size.
        /// </exception>
        public override void Remove(object key)
        {
            base.Remove(new EqualityWeakReference(key));
        }

        /// <summary>
        /// Gets or sets the <see cref="System.Object"/> with the specified key.
        /// </summary>
        /// <value></value>
        public override object this[object key]
        {
            get
            {
                object o = base[key];
                WeakReference wko = o as WeakReference;
                if (wko != null && wko.IsAlive) return wko.Target;
                return o;
            }
            set { base[new EqualityWeakReference(key)] = new WeakReference(value); }
        }

        /// <summary>
        /// Adds an element with the specified key and value into the <see cref="T:System.Collections.Hashtable"/>.
        /// </summary>
        /// <param name="key">The key of the element to add.</param>
        /// <param name="value">The value of the element to add. The value can be null.</param>
        /// <exception cref="T:System.ArgumentNullException">
        ///     <paramref name="key"/> is null.
        /// </exception>
        /// <exception cref="T:System.ArgumentException">
        /// An element with the same key already exists in the <see cref="T:System.Collections.Hashtable"/>.
        /// </exception>
        /// <exception cref="T:System.NotSupportedException">
        /// The <see cref="T:System.Collections.Hashtable"/> is read-only.
        /// -or-
        /// The <see cref="T:System.Collections.Hashtable"/> has a fixed size.
        /// </exception>
        public override void Add(object key, object value)
        {
            base.Add(new EqualityWeakReference(key), new WeakReference(value));
        }

        /// <summary>
        /// Determines whether the <see cref="T:System.Collections.Hashtable"/> contains a specific key.
        /// </summary>
        /// <param name="key">The key to locate in the <see cref="T:System.Collections.Hashtable"/>.</param>
        /// <returns>
        /// true if the <see cref="T:System.Collections.Hashtable"/> contains an element with the specified key; otherwise, false.
        /// </returns>
        /// <exception cref="T:System.ArgumentNullException">
        ///     <paramref name="key"/> is null.
        /// </exception>
        public override bool Contains(object key)
        {
            return base.Contains(key);
        }

        /// <summary>
        /// Determines whether the <see cref="T:System.Collections.Hashtable"/> contains a specific key.
        /// </summary>
        /// <param name="key">The key to locate in the <see cref="T:System.Collections.Hashtable"/>.</param>
        /// <returns>
        /// true if the <see cref="T:System.Collections.Hashtable"/> contains an element with the specified key; otherwise, false.
        /// </returns>
        /// <exception cref="T:System.ArgumentNullException">
        ///     <paramref name="key"/> is null.
        /// </exception>
        public override bool ContainsKey(object key)
        {
            return base.ContainsKey(key);
        }

        /// <summary>
        /// Determines whether the <see cref="T:System.Collections.Hashtable"/> contains a specific value.
        /// </summary>
        /// <param name="value">The value to locate in the <see cref="T:System.Collections.Hashtable"/>. The value can be null.</param>
        /// <returns>
        /// true if the <see cref="T:System.Collections.Hashtable"/> contains an element with the specified <paramref name="value"/>; otherwise, false.
        /// </returns>
        public override bool ContainsValue(object value)
        {
            return base.ContainsValue(new WeakReference(value));
        }

        /// <devdoc>
        ///     Override of Item that wraps a weak reference around the
        ///     key and performs a scavenge.
        /// </devdoc>
        public void SetWeak(object key, object value)
        {
            ScavengeKeys();
            this[new EqualityWeakReference(key)] = new WeakReference(value);
        }

        /// <devdoc>
        ///     This method checks to see if it is necessary to
        ///     scavenge keys, and if it is it performs a scan
        ///     of all keys to see which ones are no longer valid.
        ///     To determine if we need to scavenge keys we need to
        ///     try to track the current GC memory.  Our rule of
        ///     thumb is that if GC memory is decreasing and our
        ///     key count is constant we need to scavenge.  We
        ///     will need to see if this is too often for extreme
        ///     use cases like the CompactFramework (they add
        ///     custom type data for every object at design time).
        /// </devdoc>
        private void ScavengeKeys()
        {
            int hashCount = Count;

            if (hashCount == 0)
            {
                return;
            }

            if (lastHashCount == 0)
            {
                lastHashCount = hashCount;
                return;
            }

            long globalMem = GC.GetTotalMemory(false);

            if (lastGlobalMem == 0)
            {
                lastGlobalMem = globalMem;
                return;
            }

            float memDelta = (globalMem - lastGlobalMem) / (float)lastGlobalMem;
            float hashDelta = (hashCount - lastHashCount) / (float)lastHashCount;

            if (memDelta < 0 && hashDelta >= 0)
            {
                // Perform a scavenge through our keys, looking
                // for dead references.               
                ArrayList cleanupList = null;
                foreach (object o in Keys)
                {
                    EqualityWeakReference wr = o as EqualityWeakReference;
                    if (wr != null && !wr.IsAlive)
                    {
                        if (cleanupList == null)
                        {
                            cleanupList = new ArrayList();
                        }

                        cleanupList.Add(wr);
                    }
                }

                if (cleanupList != null)
                {
                    foreach (object o in cleanupList)
                    {
                        Remove(o);
                    }
                }
            }

            lastGlobalMem = globalMem;
            lastHashCount = hashCount;
        }

        #region Nested type: EqualityWeakReference

        /// <devdoc>
        ///     A wrapper of WeakReference that overrides GetHashCode and
        ///     Equals so that the weak reference returns the same equality
        ///     semantics as the object it wraps.  This will always return
        ///     the object's hash code and will return True for a Equals
        ///     comparison of the object it is wrapping.  If the object
        ///     it is wrapping has finalized, Equals always returns false.
        /// </devdoc>
        private sealed class EqualityWeakReference
        {
            private readonly int hashCode;
            private readonly WeakReference weakRef;

            internal EqualityWeakReference(object o)
            {
                weakRef = new WeakReference(o);
                hashCode = o.GetHashCode();
            }

            public bool IsAlive
            {
                get { return weakRef.IsAlive; }
            }

            public object Target
            {
                get { return weakRef.Target; }
            }

            public override bool Equals(object o)
            {
                if (o == null)
                {
                    return false;
                }

                if (o.GetHashCode() != hashCode)
                {
                    return false;
                }

                if (o == this || ReferenceEquals(o, Target))
                {
                    return true;
                }

                return false;
            }

            public override int GetHashCode()
            {
                return hashCode;
            }
        }

        #endregion

        #region Nested type: WeakKeyComparer

        private class WeakKeyComparer : IEqualityComparer
        {
            #region IEqualityComparer Members

            bool IEqualityComparer.Equals(object x, object y)
            {
                if (x == null)
                {
                    return y == null;
                }

                if (y != null && x.GetHashCode() == y.GetHashCode())
                {
                    EqualityWeakReference wX = x as EqualityWeakReference;
                    EqualityWeakReference wY = y as EqualityWeakReference;

                    //Both WeakReferences are gc'd and they both had the same hash
                    //Since this is only used in Weak Hash table races are not really an issue.
                    if (wX != null && wY != null && !wY.IsAlive && !wX.IsAlive)
                    {
                        return true;
                    }

                    if (wX != null)
                    {
                        x = wX.Target;
                    }

                    if (wY != null)
                    {

                        y = wY.Target;
                    }

                    return Equals(x, y);
                }

                return false;
            }

            int IEqualityComparer.GetHashCode(object obj)
            {
                return obj.GetHashCode();
            }

            #endregion
        }

        #endregion
    }
}
